Catalyst / admin/Strike 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Strike

public

Web-Based UK Cyber Compliance Tool with Reporting

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
Strike / StrikeXi v3 / backend / app / main.py 3226 B · main
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import time

from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import text
from sqlalchemy.exc import OperationalError

from .config import settings
from .database import engine, SessionLocal, Base
from . import models
from .security import hash_password, verify_password
from .routers import auth, catalogue, assessments, audit, users, frameworks

app = FastAPI(title="StrikeXi API", version="3.0.0",
              description="Multi-framework Cyber Maturity Assessment platform "
                          "(NCSC CAF, UK Cyber Security & Resilience Bill)")

app.add_middleware(
    CORSMiddleware,
    allow_origins=[o.strip() for o in settings.CORS_ORIGINS.split(",")],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

app.include_router(auth.router)
app.include_router(users.router)
app.include_router(frameworks.router)
app.include_router(catalogue.router)
app.include_router(assessments.router)
app.include_router(audit.router)


def _wait_for_db(retries: int = 30):
    for i in range(retries):
        try:
            with engine.connect() as conn:
                conn.execute(text("SELECT 1"))
            return
        except OperationalError:
            time.sleep(2)
    raise RuntimeError("Database not reachable after retries")


@app.on_event("startup")
def on_startup():
    _wait_for_db()
    # Schema is created by SQL init scripts; ensure ORM tables exist too (idempotent).
    Base.metadata.create_all(bind=engine)
    # Seed / self-heal the admin user (bcrypt).
    db = SessionLocal()
    try:
        existing = db.query(models.User).filter(
            models.User.username == settings.ADMIN_USERNAME).first()
        if not existing:
            # First boot on a fresh database: create the admin and force a real
            # password to be set on first login.
            db.add(models.User(
                username=settings.ADMIN_USERNAME,
                password_hash=hash_password(settings.ADMIN_PASSWORD),
                full_name="Administrator", role="admin", is_active=True,
                must_change_password=True,
            ))
            db.commit()
        else:
            # Self-heal the first-login admin setup. The DB volume persists across
            # rebuilds, so the admin isn't re-seeded — but if it STILL uses the
            # default configured password it was never personalised. Re-arm the
            # forced password change so the first-login admin setup reappears.
            # An admin who has already set a real password is left untouched.
            still_default = verify_password(settings.ADMIN_PASSWORD, existing.password_hash)
            changed = False
            if still_default and not existing.must_change_password:
                existing.must_change_password = True
                changed = True
            # Never leave the only administrator disabled/locked out of setup.
            if still_default and not existing.is_active:
                existing.is_active = True
                changed = True
            if changed:
                db.commit()
    finally:
        db.close()


@app.get("/api/health")
def health():
    return {"status": "ok"}